package ru.myrtle.jst.parser;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import ru.myrtle.jst.ast.Unit;
import ru.myrtle.jst.i18n.NLS;
import ru.myrtle.jst.Problem;
import ru.myrtle.jst.Syntax;
import ru.myrtle.jst.ast.Comment;
import ru.myrtle.jst.ast.ConstructorParameter;
import ru.myrtle.jst.ast.Declaration;
import ru.myrtle.jst.ast.Expression;
import ru.myrtle.jst.ast.Include;
import ru.myrtle.jst.ast.Invoke;
import ru.myrtle.jst.ast.Parameter;
import ru.myrtle.jst.ast.Scriptlet;
import ru.myrtle.jst.ast.Text;
import ru.myrtle.jst.store.Template;

public class Parser {

    private static final String STRINGS_DELIMITER = "\\s+";

    private static final String STR_CONTROL = "%";
    private static final String STR_COMMENT = "--";
    private static final String STR_DECLARATION = "!";
    private static final String STR_DIRECTIVE = "@";
    private static final String STR_EXPRESSION = "=";

    private static final String DIRECTIVE_JET = "jet";
    private static final String DIRECTIVE_JET__PACKAGE = "package";
    private static final String DIRECTIVE_JET__IMPORTS = "imports";
    private static final String DIRECTIVE_JET__CLASS = "class";
    private static final String DIRECTIVE_JET__EXTENDS = "extends";
    private static final String DIRECTIVE_JET__IMPLEMENTS = "implements";

    private static final String DIRECTIVE_IMPORT = "import";
    private static final String DIRECTIVE_IMPORT__NAME = "name";

    private static final String DIRECTIVE_PARAMETER = "parameter";
    private static final String DIRECTIVE_PARAMETER__NAME = "name";
    private static final String DIRECTIVE_PARAMETER__TYPE = "type";

    private static final String DIRECTIVE_THROWS = "throws";
    private static final String DIRECTIVE_THROWS__EXCEPTIONS = "exceptions";

    private static final String DIRECTIVE_INCLUDE = "include";
    private static final String DIRECTIVE_INCLUDE__FILE = "file";
    private static final String DIRECTIVE_INCLUDE__INDENT = "indent";

    private static final String DIRECTIVE_INVOKE = "invoke";
    private static final String DIRECTIVE_INVOKE__FILE = "file";
    private static final String DIRECTIVE_INVOKE__PARAMETERS = "parameters";
    private static final String DIRECTIVE_INVOKE__INDENT = "indent";

    private static final String DIRECTIVE_IMPLEMENTS = "implements";
    private static final String DIRECTIVE_IMPLEMENTS__NAME = "name";

    private static final String DIRECTIVE_CONSTRUCTOR_PARAMETER = "constructor-parameter";
    private static final String DIRECTIVE_CONSTRUCTOR_PARAMETER__NAME = "name";
    private static final String DIRECTIVE_CONSTRUCTOR_PARAMETER__TYPE = "type";

    private static final Directive JET = new Directive(
            DIRECTIVE_JET,
            new Attribute(DIRECTIVE_JET__PACKAGE, true),
            new Attribute(DIRECTIVE_JET__IMPORTS, false),
            new Attribute(DIRECTIVE_JET__CLASS, true),
            new Attribute(DIRECTIVE_JET__EXTENDS, false),
            new Attribute(DIRECTIVE_JET__IMPLEMENTS, false)
    );

    private static final Directive IMPORT = new Directive(
            DIRECTIVE_IMPORT,
            new Attribute(DIRECTIVE_IMPORT__NAME, true)
    );

    private static final Directive PARAMETER = new Directive(
            DIRECTIVE_PARAMETER,
            new Attribute(DIRECTIVE_PARAMETER__NAME, true),
            new Attribute(DIRECTIVE_PARAMETER__TYPE, true)
    );

    private static final Directive THROWS = new Directive(
            DIRECTIVE_THROWS,
            new Attribute(DIRECTIVE_THROWS__EXCEPTIONS, true)
    );

    private static final Directive INCLUDE = new Directive(
            DIRECTIVE_INCLUDE,
            new Attribute(DIRECTIVE_INCLUDE__FILE, true),
            new Attribute(DIRECTIVE_INCLUDE__INDENT, false)
    );

    private static final Directive INVOKE = new Directive(
            DIRECTIVE_INVOKE,
            new Attribute(DIRECTIVE_INVOKE__FILE, true),
            new Attribute(DIRECTIVE_INVOKE__PARAMETERS, false),
            new Attribute(DIRECTIVE_INVOKE__INDENT, false)
    );

    private static final Directive IMPLEMENTS = new Directive(
            DIRECTIVE_IMPLEMENTS,
            new Attribute(DIRECTIVE_IMPLEMENTS__NAME, true)
    );

    private static final Directive CONSTRUCTOR_PARAMETER = new Directive(
            DIRECTIVE_CONSTRUCTOR_PARAMETER,
            new Attribute(DIRECTIVE_CONSTRUCTOR_PARAMETER__NAME, true),
            new Attribute(DIRECTIVE_CONSTRUCTOR_PARAMETER__TYPE, true)
    );

    private final static Directive[] DIRECTIVES = new Directive[] {
            JET,
            IMPORT,
            PARAMETER,
            THROWS,
            INCLUDE,
            INVOKE,
            IMPLEMENTS,
            CONSTRUCTOR_PARAMETER
    };

    /*
     *
     */

    private static class Directive {

        final String name;
        final Set<Attribute> attributes;

        Directive(String name, Attribute... attributes) {
            this.name = name;
            this.attributes = new HashSet<Attribute>(
                    Arrays.asList(attributes)
            );
        }

        void validateAttributes(Map<String, String> values, Mark where) throws SyntaxException {
            for (Attribute attribute : attributes) {
                String value = values.get(attribute.name);

                if (attribute.required && value == null) {
                    throw new SyntaxException(
                            String.format(
                                    NLS.BUNDLE.getString(NLS.KEY_ATTRIBUTE_MISSING), attribute.name
                            ),
                            where
                    );
                }
            }

            Set<String> actualNames = new HashSet<String>(
                    values.keySet()
            );

            for (Attribute attribute : attributes) {
                actualNames.remove(attribute.name);
            }

            if (actualNames.size() > 0) {
                StringBuilder builder = new StringBuilder();

                for (String name : actualNames) {
                    if (builder.length() > 0) {
                        builder.append(", ");
                    }
                    builder.append(name);
                }

                throw new SyntaxException(
                        String.format(
                                NLS.BUNDLE.getString(NLS.KEY_ILLEGAL_ATTRIBUTES), name, builder.toString()
                        ),
                        where
                );
            }
        }
    }

    private static class Attribute {

        final String name;
        final boolean required;

        Attribute(String name, boolean required) {
            this.name = name;
            this.required = required;
        }
    }

    /*
     *
     */

    private interface Agent {

        public boolean accept(Buffer buffer, Unit unit);
    }

    /*
     *
     */

    private class CommentAgent implements Agent {

        public boolean accept(Buffer buffer, Unit unit) {
            if (matches(buffer, commentBegin)) {
                Mark elementStart = buffer.mark();

                buffer.advance(
                        commentBegin.length()
                );

                Mark start = buffer.mark();
                Mark stop = skipUntil(
                        buffer, commentEnd
                );

                if (stop == null) {
                    recordUnterminatedElement(
                            unit, commentBegin, commentEnd, elementStart
                    );
                }
                else {
                    // Eat newline after closing tag.
                    consumeNewLine(buffer);

                    String text = buffer.getString(
                            start.getPosition(), stop.getPosition() - commentEnd.length()
                    );

                    unit.addElements(
                            new Comment(
                                    unit,
                                    start.getLine(),
                                    start.getColumn(),
                                    text
                            )
                    );
                }

                return true;
            }
            else {
                return false;
            }
        }
    }

    /*
     *
     */

    private class DeclarationAgent implements Agent {

        public boolean accept(Buffer buffer, Unit unit) {
            if (matches(buffer, declarationBegin)) {
                Mark elementStart = buffer.mark();

                buffer.advance(
                        declarationBegin.length()
                );

                Mark start = buffer.mark();
                Mark stop = skipUntil(
                        buffer, declarationEnd
                );

                if (stop == null) {
                    recordUnterminatedElement(
                            unit, declarationBegin, declarationEnd, elementStart
                    );
                }
                else {
                    // Eat newline after closing tag.
                    consumeNewLine(buffer);

                    String text = buffer.getString(
                            start.getPosition(), stop.getPosition() - declarationEnd.length()
                    );

                    unit.addElements(
                            new Declaration(
                                    unit,
                                    start.getLine(),
                                    start.getColumn(),
                                    text
                            )
                    );
                }

                return true;
            }
            else {
                return false;
            }
        }
    }

    /*
     *
     */

    private class DirectiveAgent implements Agent {

        public boolean accept(Buffer buffer, Unit unit) {
            if (matches(buffer, directiveBegin)) {
                Mark start = buffer.mark();
                try {
                    buffer.advance(
                            directiveBegin.length()
                    );
                    skipSpaces(buffer);

                    // Check which directive it is.

                    Directive match = null;

                    for (Directive directive : DIRECTIVES) {
                        if (matches(buffer, directive.name)) {
                            match = directive;
                            break;
                        }
                    }

                    if (match == null) {
                        throw new SyntaxException(
                                NLS.BUNDLE.getString(NLS.KEY_UNKNOWN_DIRECTIVE), start
                        );
                    }

                    buffer.advance(
                            match.name.length()
                    );

                    // Parse the attr-val pairs.

                    Map<String, String> attributes = parseTagAttributes(buffer);

                    match.validateAttributes(
                            attributes, start
                    );

                    // Match close.

                    skipSpaces(buffer);

                    if (matches(buffer, directiveEnd)) {
                        buffer.advance(
                                directiveEnd.length()
                        );

                        // Eat newline after closing tag.
                        consumeNewLine(buffer);
                    }
                    else {
                        recordUnterminatedElement(
                                unit, directiveBegin, directiveEnd, start
                        );
                    }

                    if (match == JET) {
                        String a_package = attributes.get(DIRECTIVE_JET__PACKAGE);
                        String a_imports = attributes.get(DIRECTIVE_JET__IMPORTS);
                        String a_class = attributes.get(DIRECTIVE_JET__CLASS);
                        String a_extends = attributes.get(DIRECTIVE_JET__EXTENDS);
                        String a_implements = attributes.get(DIRECTIVE_JET__IMPLEMENTS);

                        if (a_package != null) {
                            unit.setOutputPackage(a_package);
                        }
                        if (a_imports != null) {
                            unit.addImports(
                                    a_imports.split(STRINGS_DELIMITER)
                            );
                        }
                        if (a_class != null) {
                            unit.setOutputClassName(a_class);
                        }
                        if (a_extends != null) {
                            unit.setOutputClassExtends(a_extends);
                        }
                        if (a_implements != null) {
                            unit.addOutputClassImplements(
                                    a_implements.split(STRINGS_DELIMITER)
                            );
                        }
                    }
                    else if (match == IMPORT) {
                        unit.addImports(
                                attributes.get(DIRECTIVE_IMPORT__NAME).split(STRINGS_DELIMITER)
                        );
                    }
                    else if (match == PARAMETER) {
                        unit.addParameters(
                                new Parameter(
                                        attributes.get(DIRECTIVE_PARAMETER__NAME),
                                        attributes.get(DIRECTIVE_PARAMETER__TYPE)
                                )
                        );
                    }
                    else if (match == THROWS) {
                        String a_throws = attributes.get(DIRECTIVE_THROWS__EXCEPTIONS);

                        unit.addExceptions(
                                a_throws.split(STRINGS_DELIMITER)
                        );
                    }
                    else if (match == INCLUDE) {
                        unit.addElements(
                                new Include(
                                        unit,
                                        start.getLine(),
                                        start.getColumn(),
                                        attributes.get(DIRECTIVE_INCLUDE__FILE)
                                )
                        );
                    }
                    else if (match == INVOKE) {
                        unit.addElements(
                                new Invoke(
                                        unit,
                                        start.getLine(),
                                        start.getColumn(),
                                        attributes.get(DIRECTIVE_INVOKE__FILE),
                                        attributes.get(DIRECTIVE_INVOKE__PARAMETERS)
                                )
                        );
                    }
                    else if (match == IMPLEMENTS) {
                        unit.addOutputClassImplements(
                                attributes.get(DIRECTIVE_IMPLEMENTS__NAME).split(STRINGS_DELIMITER)
                        );
                    }
                    else if (match == CONSTRUCTOR_PARAMETER) {
                        unit.addConstructorParameters(
                                new ConstructorParameter(
                                        attributes.get(DIRECTIVE_CONSTRUCTOR_PARAMETER__NAME),
                                        attributes.get(DIRECTIVE_CONSTRUCTOR_PARAMETER__TYPE)
                                )
                        );
                    }

                    return true;
                }
                catch (SyntaxException thrown) {
                    unit.addProblem(
                            new Problem(
                                    unit,
                                    Problem.Severity.ERROR,
                                    thrown.getMessage(),
                                    thrown.getWhere().getLine(),
                                    thrown.getWhere().getColumn()
                            )
                    );

                    skipUntil(
                            buffer, directiveEnd
                    );

                    return true;
                }
            }
            else {
                return false;
            }
        }
    }

    /*
     *
     */

    private class ExpressionAgent implements Agent {

        public boolean accept(Buffer buffer, Unit unit) {
            if (matches(buffer, expressionBegin)) {
                buffer.advance(
                        expressionBegin.length()
                );

                Mark start = buffer.mark();
                Mark stop = skipUntil(
                        buffer, expressionEnd
                );

                if (stop == null) {
                    recordUnterminatedElement(
                            unit, expressionBegin, expressionEnd, start
                    );
                }
                else {
                    String text = buffer.getString(
                            start.getPosition(), stop.getPosition() - expressionEnd.length()
                    );

                    unit.addElements(
                            new Expression(
                                    unit,
                                    start.getLine(),
                                    start.getColumn(),
                                    text
                            )
                    );
                }

                return true;
            }
            else {
                return false;
            }
        }
    }

    /*
     *
     */

    private class ScriptletAgent implements Agent {

        public boolean accept(Buffer buffer, Unit unit) {
            if (matches(buffer, scriptletBegin)) {
                buffer.advance(scriptletBegin.length());

                Mark start = buffer.mark();
                Mark stop = skipUntil(
                        buffer, scriptletEnd
                );

                if (stop == null) {
                    recordUnterminatedElement(
                            unit, scriptletBegin, scriptletEnd, start
                    );
                }
                else {
                    // Eat newline after closing tag.
                    consumeNewLine(buffer);

                    String text = buffer.getString(
                            start.getPosition(), stop.getPosition() - scriptletEnd.length()
                    );

                    unit.addElements(
                            new Scriptlet(
                                    unit,
                                    start.getLine(),
                                    start.getColumn(),
                                    text
                            )
                    );
                }

                return true;
            }
            else {
                return false;
            }
        }
    }

    /*
     *
     */

    private class TextAgent implements Agent {

        @Override
        public boolean accept(Buffer buffer, Unit unit) {
            if (buffer.reading()) {
                Mark start = buffer.mark();

                buffer.advance(1);

                while (buffer.reading()) {
                    int ch = buffer.current();

                    if (ch == begin) {
                        String text = buffer.getString(
                                start.getPosition(), buffer.mark().getPosition()
                        );

                        addText(
                                unit, start, text
                        );

                        return true;
                    }
                    else {
                        buffer.advance(1);
                    }
                }

                Mark end = buffer.mark();

                if (end.getPosition() - start.getPosition() > 0) {
                    String text = buffer.getString(
                            start.getPosition(), end.getPosition()
                    );

                    addText(
                            unit, start, text
                    );

                    return true;
                }

            }

            return false;
        }

        void addText(Unit unit, Mark start, String text) {
            unit.addElements(
                    new Text(
                            unit,
                            start.getLine(),
                            start.getColumn(),
                            text
                    )
            );
        }
    }

    /*
     *
     */

    private final Agent[] agents;

    private final char begin;
    private final char end;

    private final String commentBegin;
    private final String commentEnd;

    private final String declarationBegin;
    private final String declarationEnd;

    private final String directiveBegin;
    private final String directiveEnd;

    private final String expressionBegin;
    private final String expressionEnd;

    private final String scriptletBegin;
    private final String scriptletEnd;

    /*
     *
     */

    public Parser(Syntax syntax) {
        this.begin = syntax.getBegin();
        this.end = syntax.getEnd();

        String beginString = String.valueOf(begin);
        String endString = String.valueOf(end);

        this.commentBegin = beginString + STR_CONTROL + STR_COMMENT;
        this.commentEnd = STR_COMMENT + STR_CONTROL + endString;

        this.declarationBegin = beginString + STR_CONTROL + STR_DECLARATION;
        this.declarationEnd = STR_CONTROL + endString;

        this.directiveBegin = beginString + STR_CONTROL + STR_DIRECTIVE;
        this.directiveEnd = STR_CONTROL + endString;

        this.expressionBegin = beginString + STR_CONTROL + STR_EXPRESSION;
        this.expressionEnd = STR_CONTROL + endString;

        this.scriptletBegin = beginString + STR_CONTROL;
        this.scriptletEnd = STR_CONTROL + endString;

        agents = new Agent[] {
                new CommentAgent(),
                new DeclarationAgent(),
                new DirectiveAgent(),
                new ExpressionAgent(),
                new ScriptletAgent(),
                new TextAgent()
        };
    }

    /*
     *
     */

    private char next(Buffer buffer) {
        buffer.advance();
        if (buffer.reading()) {
            return buffer.current();
        }
        else {
            return (char) -1;
        }
    }

    private void skipSpaces(Buffer buffer) {
        while (buffer.reading() && Character.isSpaceChar(buffer.current())) {
            buffer.advance();
        }
    }

    /**
     * Parse utils - Is current character a token delimiter ?
     * Delimiters are currently defined to be =, &gt;, &lt;, ", and ' or any
     * any space character as defined by <code>isSpace</code>.
     *
     * @param buffer input buffer.
     * @return A boolean.
     */
    private boolean isDelimiter(Buffer buffer) {
        final int endTagFinalChar = end;

        if (buffer.reading()) {
            int ch = buffer.current();

            if (Character.isSpaceChar(ch)) {
                return true;
            }
            else {
                // Look for a single-char work delimiter:
                if (ch == '=' ||
                        ch == endTagFinalChar ||
                        ch == '"' ||
                        ch == '\'' ||
                        ch == '/' ||
                        ch == '>') {
                    return true;
                }

                // Look for an end-of-comment or end-of-tag:
                if (ch == '-') {
                    Mark mark = buffer.mark();

                    buffer.advance();

                    if (((ch = buffer.current()) == endTagFinalChar) || ((ch == '-') && (buffer.current() == endTagFinalChar))) {
                        buffer.reset(mark);
                        return true;
                    }
                    else {
                        buffer.reset(mark);
                        return false;
                    }
                }

                return false;
            }
        }
        else {
            return false;
        }
    }

    /**
     * Parse a space delimited token. If quoted the token will consume all
     * characters up to a matching quote, otherwise, it consumes up to the
     * first delimiter character.
     *
     * @param buffer input buffer.
     * @param quoted If <strong>true</strong> accept quoted strings.
     * @param skipSpaces if <code>true</code>, skip leading spaces to find
     * the token, false otherwise.
     *
     * @return the token, or an empty string if no token is found.
     * @throws SyntaxException when syntax is incorrect.
     */
    private String parseToken(Buffer buffer, boolean quoted, boolean skipSpaces) throws SyntaxException {
        final char endTagFinalChar = end;
        final char endTagInitialChar = STR_CONTROL.charAt(0);

        StringBuilder token = new StringBuilder();

        if (skipSpaces) {
            skipSpaces(buffer);
        }
        token.setLength(0);

        char ch = buffer.current();

        if (quoted) {
            if (ch == '"' || ch == '\'') {
                char endQuote = (ch == '"' ? '"' : '\'');

                // Consume the open quote:
                buffer.advance();

                for (; buffer.reading() && buffer.current() != endQuote; buffer.advance()) {
                    ch = buffer.current();

                    if (ch == '\\') {
                        ch = next(buffer);
                    }

                    token.append(ch);
                }

                // Check end of quote, skip closing quote:

                if (buffer.reading()) {
                    buffer.advance();
                }
                else {
                    throw new SyntaxException(
                            NLS.BUNDLE.getString(NLS.KEY_QUOTES_UNTERMINATED), buffer.mark()
                    );
                }
            }
            else {
                throw new SyntaxException(
                        NLS.BUNDLE.getString(NLS.KEY_ATTRIBUTE_QUOTED), buffer.mark()
                );
            }
        }
        else {
            if (!isDelimiter(buffer)) {
                // Read value until delimiter is found:
                for (; buffer.reading() && !isDelimiter(buffer); buffer.advance()) {
                    ch = buffer.current();

                    // Take care of the quoting here.
                    if (ch == '\\') {
                        buffer.advance();

                        if (buffer.current() == '"' ||
                                buffer.current() == '\'' ||
                                buffer.current() == endTagFinalChar ||
                                buffer.current() == endTagInitialChar ||
                                buffer.current() == '>') {
                            ch = buffer.current();
                        }
                    }

                    token.append(ch);
                }
            }
        }

        return token.toString();
    }

    /**
     * Parse a space delimited token. If quoted the token will consume all
     * characters up to a matching quote, otherwise, it consumes up to the
     * first delimiter character.
     *
     * @param buffer input buffer.
     * @param quoted If <strong>true</strong> accept quoted strings.
     * @return the token, or an empty string if no token is found.
     * @throws SyntaxException when syntax is incorrect.
     */
    private String parseToken(Buffer buffer, boolean quoted) throws SyntaxException {
        return parseToken(
                buffer, quoted, true /* skip spaces*/
        );
    }

    /**
     * Parse an attribute/value pair, and store it in provided hash table.
     * The attribute/value pair is defined by:
     * <pre>
     * av := spaces token spaces '=' spaces token spaces
     * </pre>
     * Where <em>token</em> is defined by <code>parseToken</code> and
     * <em>spaces</em> is defined by <code>skipSpaces</code>.
     * The name is always considered case insensitive, hence stored in its
     * lower case version.
     *
     * @param buffer input buffer.
     * @param into The Map instance to save the result to.
     * @throws SyntaxException when syntax is incorrect.
     */
    private void parseAttributeValue(Buffer buffer, Map<String, String> into) throws SyntaxException {
        // Get the attribute name:
        skipSpaces(buffer);
        String name = parseToken(buffer, false);

        // Check for an equal sign:

        skipSpaces(buffer);

        if (buffer.current() != '=') {
            throw new SyntaxException(
                    String.format(
                            NLS.BUNDLE.getString(NLS.KEY_ATTRIBUTE_NO_VALUE), name
                    ),
                    buffer.mark()
            );
        }

        buffer.advance();

        // Get the attribute value:

        skipSpaces(buffer);
        String value = parseToken(buffer, true);
        skipSpaces(buffer);

        // Add the binding to the provided hashtable:

        into.put(name, value);
    }

    /**
     * Parse some tag attributes. The stream is assumed to be positioned right
     * after the tag name. The syntax recognized is:
     *
     * <pre>
     *  tag-attrs := empty | attr-list (&quot;&gt;&quot; | &quot;--&gt;&quot; | %&gt;)
     *  attr-list := empty | av spaces attr-list
     *  empty     := spaces
     * </pre>
     *
     * Where <em>av</em> is defined by <code>parseAttributeValue</code>.
     *
     * @param buffer input buffer.
     * @return name-value pairs.
     * @throws SyntaxException when syntax is incorrect.
     */
    private Map<String, String> parseTagAttributes(Buffer buffer) throws SyntaxException {
        final int endTagFinalChar = end;
        final int endTagInitialChar = STR_CONTROL.charAt(0);

        Map<String, String> values = new LinkedHashMap<String, String>();

        while (buffer.reading()) {
            skipSpaces(buffer);

            if (buffer.reading()) {
                int ch = buffer.current();
                if (ch == endTagFinalChar) {
                    return values;
                }

                if (ch == '-') {
                    Mark mark = buffer.mark();

                    // Close NCSA like attributes "->"
                    try {
                        if (next(buffer) == '-' && next(buffer) == endTagFinalChar) {
                            return values;
                        }
                    }
                    finally {
                        buffer.reset(mark);
                    }
                }
                else if (ch == endTagInitialChar) {
                    Mark mark = buffer.mark();

                    // Close variable like attributes "%>"
                    try {
                        if (next(buffer) == endTagFinalChar) {
                            return values;
                        }
                    }
                    finally {
                        buffer.reset(mark);
                    }
                }
                else if (ch == '/') {
                    Mark mark = buffer.mark();

                    // XMLesque Close tags
                    try {
                        if (next(buffer) == endTagFinalChar) {
                            return values;
                        }
                    }
                    finally {
                        buffer.reset(mark);
                    }
                }
                else if (Character.isLetter(ch)) {
                    // Parse as an attribute=value:
                    parseAttributeValue(buffer, values);
                }
                else {
                    return values;
                }
            }
        }

        return values;
    }

    /**
     * Skip until the given string is matched in the stream. When returned,
     * the context is positioned past the end of the match.
     *
     * @param buffer input buffer.
     * @param limit The String to match.
     * @return A non-null <code>Mark</code> instance if found,
     * <strong>null</strong> otherwise.
     */
    private Mark skipUntil(Buffer buffer, String limit) {
        int limlen = limit.length();
        int ch;

        while (buffer.reading()) {
            ch = buffer.current();

            if (ch == limit.charAt(0)) {
                Mark mark = buffer.mark();

                int i = 0;
                for (; i < limlen && buffer.reading(); i++, buffer.advance()) {
                    ch = buffer.current();

                    if (Character.toLowerCase(ch) != limit.charAt(i)) {
                        break;
                    }
                }

                if (i == limlen) {
                    return buffer.mark();
                }
                else {
                    buffer.reset(mark);
                }

            }

            buffer.advance();
        }

        return null;
    }

    private void consumeNewLine(Buffer buffer) {
        if (buffer.reading()) {
            char ch1 = buffer.current();

            if (ch1 == '\r') {
                buffer.advance();

                if (buffer.reading()) {
                    char ch2 = buffer.current();

                    if (ch2 == '\n') {
                        buffer.advance();
                    }
                }
            }
            else if (ch1 == '\n') {
                buffer.advance();
            }
        }
    }

    private boolean matches(Buffer buffer, String string) {
        Mark mark = buffer.mark();

        for (int i = 0, length = string.length(); i < length; i++) {
            if (buffer.reading()) {
                int ch = buffer.current();

                if (ch != string.charAt(i)) {
                    buffer.reset(mark);
                    return false;
                }

                buffer.advance();
            }
            else {
                buffer.reset(mark);
                return false;
            }
        }

        buffer.reset(mark);

        return true;
    }

    /*
     *
     */

    private void recordUnterminatedElement(Unit unit, String openSequence, String expectedCloseSequence, Mark where) {
        unit.addProblem(
                new Problem(
                        unit,
                        Problem.Severity.ERROR,
                        String.format(
                                NLS.BUNDLE.getString(NLS.KEY_UNTERMINATED), expectedCloseSequence, openSequence
                        ),
                        where.getLine(),
                        where.getColumn()
                )
        );
    }

    /*
     *
     */

    public Unit parse(Template template) throws IOException {
        Unit unit = new Unit(template);

        Buffer buffer = new Buffer();
        buffer.fill(template);

        while (buffer.reading()) {
            boolean accepted = false;

            for (Agent agent : agents) {
                accepted = agent.accept(buffer, unit);

                if (accepted) {
                    break;
                }
            }

            if (!accepted) {
                break;
            }
        }

        return unit;
    }
}
